<?php
/*
 * Restful API
 */

class ApiController extends Controller
{
	/**
	 * @var ApiToken
	 */
	public $token;

	/**
	 * - token 表 -
	 * 存储会话记录, 至少包含以下字段:
	 * user_id: 相关用户名或用户 ID
	 * token: 长度 32 的字符串, 根据登录成功时的时间/用户名/IP等因素生成, 要求全表唯一不重复
	 * ip: 登录成功时的 ip 地址
	 * time: 登录成功时的时间(秒数)
	 *
	 * - log 表 -
	 * 存储接口调用日志, 至少包含以下字段:
	 * token: 对应的 token 表的 ID 或 token
	 * time: 操作时间
	 * content: 操作内容或事件，如: login, logout, clean, put(key1,key2,...), del(...), search(...), count(...)=??
	 *
	 * - 接口设计 -
	 * 所有调用操作均要求 POST 动作, 失败或错误时 HTTP 状态码为 200 以外的其它值。
	 * 成功时响应数据均为 JSON 格式。如为 GET 则在 xsmanager 后台兼容性的显示对应的 API 说明
	 *
	 * - XSMANAGER 后台 -
	 * 在后台正常的列出所用接口，内容为显示使用说明，无需权限检查。
	 *
	 * 除此之外，还应列出当前用户名下的 token 记录，并提示是否有效（根据最近登录成功时间，以3600 秒为超时？）
	 * 列出用户名下的操作日志列表供查阅（TODO）
	 *
	 * 以下具体接口用法分批讲述。
	 *
	 * 说明文档采用 <h2> <h3> + <ul> + <blockquote> + <code> 标签简要说明即可。
	 *
	 */
	public function init()
	{
		parent::init();
		Yii::app()->errorHandler->errorAction = 'api/error';
	}

	/**
	 * 过滤器
	 * 在这个过滤器中, 若为 POST action 则读取 access_token 加载用户, 并读取 project 加以权限判断
	 * 如不正确则直接抛出对应的 HttpException (logout 不需要 project 判断)
	 * token 无效或过期响应 401 请求
	 * token 对应的用户权限不足以操作当前 api 的相关 action, 则给出 403
	 *
	 * 如果只是 GET action 则按原先方式判断请求, 仅用于页面展示 ...
	 * @return array
	 */
	public function filters()
	{
		if (Yii::app()->request->isPostRequest) {
			return array(
				array('application.components.ApiAccessFilter - login - error'),
			);
		}
		return parent::filters();
	}

	/**
	 * @param CAction $action
	 */
	public function beforeAction($action)
	{
		if (!Yii::app()->request->isPostRequest) {
			$this->breadcrumbs = array(
				$this->projectName => array('project/schema'), 'API: ' . $action->id,
			);
			$this->render($action->id);
			return false;
		}
		return parent::beforeAction($action);
	}

	/**
	 * 首页
	 */
	public function actionIndex()
	{
		
	}

	/**
	 *  登入
	 */
	public function actionLogin()
	{
		try {
			$user = User::model()->findByAttributes(array(
				'username' => Yii::app()->request->getPost('username', ''),
			));
			if ($user === null) {
				throw new CHttpException(404, 'Username not exists');
			}
			if ($user->password !== Yii::app()->request->getPost('password')) {
				throw new CHttpException(400, 'Password incorrect');
			}
			if (($token = $user->last_token) === null) {
				$token = new ApiToken();
				$token->user_id = $user->id;
			}
			$token->save(false);
			echo CJSON::encode(array(
				'result' => true,
				'access_token' => $token->token,
				'expires_in' => ApiToken::EXPIRES_IN
			));
		} catch (CDbException $e) {
			throw new CHttpException(500, $e->getMessage());
		}
	}

	/**
	 * 登出 
	 */
	public function actionLogout()
	{
		try {
			$this->token->logout();
			echo CJSON::encode(array('result' => true));
		} catch (CDbException $e) {
			throw new CHttpException(500, $e->getMessage());
		}
	}

	/**
	 *  提交数据 
	 */
	public function actionPut()
	{
		try {
			$model = new DocModel($this->projectName);
			$xs = $model->getXS();
			$fid = $xs->getFieldId()->name;
			$data = Yii::app()->request->getPost('data');
			$count = 0;
			if (!isset($data[$fid])) {
				throw new CHttpException(416, 'Missing primary key');
			}
			// reformat the data
			if (!is_array($data[$fid])) {
				$items = array($data);
			} else {
				$items = array();
				foreach ($data as $key => $values) {
					foreach ($values as $i => $value) {
						if (!isset($items[$i])) {
							$items[$i] = array();
						}
						$items[$i][$key] = $value;
					}
				}
			}
			// submit index
			$index = $xs->index;
			$index->openBuffer();
			foreach ($items as $item) {
				if (!isset($item[$fid])) {
					continue;
				}
				$doc = new XSDocument($item);
				$index->update($doc);
				$model->delRemovedCache($item[$fid]);
				$count++;
			}
			$index->closeBuffer();
			$index->flushIndex();
			echo CJSON::encode(array('result' => true, 'count' => $count));
		} catch (XSException $e) {
			throw new CHttpException(500, $e->getMessage());
		}
	}

	/**
	 * 根据主键删除数据
	 */
	public function actionDel()
	{
		try {
			$id = Yii::app()->request->getPost('id');
			if ($id === null) {
				throw new CHttpException(406, 'Wrong parameter');
			}
			$model = new DocModel($this->projectName);
			$count = is_array($id) ? count($id) : 1;
			$model->deleteByPk($id);
			echo CJSON::encode(array('result' => true, 'count' => $count));
		} catch (XSException $e) {
			throw new CHttpException(500, $e->getMessage());
		}
	}

	/**
	 * 清空索引
	 */
	public function actionClean()
	{
		try {
			$model = new DocModel($this->projectName);
			$model->clean();
			echo CJSON::encode(array('result' => true));
		} catch (XSException $e) {
			$code = 500;
			if ($e->getCode() === CMD_ERR_REBUILDING) {
				$code = 423;
			}
			throw new CHttpException($code, $e->getMessage());
		}
	}

	/**
	 * 重建索引
	 */
	public function actionRebuild()
	{
		try {
			/* @var $index XSIndex */
			$index = Yii::app()->pf->get($this->projectName)->index;
			switch (Yii::app()->request->getPost('type')) {
				case 'begin':
					$index->execCommand(array('cmd' => CMD_INDEX_REBUILD, 'arg1' => 0), CMD_OK_DB_REBUILD);
					break;
				case 'stop':
					$index->stopRebuild();
					break;
				case 'end':
					$index->execCommand(array('cmd' => CMD_INDEX_REBUILD, 'arg1' => 1), CMD_OK_DB_REBUILD);
					break;
				default :
					throw new CHttpException(406, 'Incorrect value of type');
			}
			echo CJSON::encode(array('result' => true));
		} catch (XSException $e) {
			$code = 500;
			if ($e->getCode() === CMD_ERR_REBUILDING || $e->getCode() === CMD_ERR_WRONGPLACE) {
				$code = 423;
			}
			throw new CHttpException($code, $e->getMessage());
		}
	}

	/**
	 * 普通搜索
	 */
	public function actionSearch()
	{
		try {
			$result = array('result' => true);
			// parameters
			$request = Yii::app()->request; /* @var $request CHttpRequest */
			$fuzzy = $request->getPost('fuzzy', 'no');
			$cutPercent = $request->getPost('cut_percent', 0);
			$cutWeight = $request->getPost('cut_weight', 0);
			$synonym = $request->getPost('synonym', 'no');
			$sort = $request->getPost('sort', '');
			$collapse = $request->getPost('collapse', '');
			$range = $request->getPost('range', '');
			$scwsMulti = $request->getPost('scws_multi', 3);
			$limit = $request->getPost('limit', 10);
			$offset = $request->getPost('offset', 0);
			$related = $request->getPost('related', 'no');
			$corrected = $request->getPost('corrected', 'no');
			$query = $request->getPost('query', '');
			$highlight = $request->getParam('highlight', '');
			// search
			$search = Yii::app()->pf->get($this->projectName)->search; /* @var $search XSSearch */
			$search->setFuzzy($fuzzy === 'yes');
			if ($cutPercent !== 0 || $cutWeight !== 0) {
				$search->setCutOff($cutPercent, $cutWeight);
			}
			$search->setAutoSynonyms($synonym === 'yes');
			if ($synonym === 'yes' && isset($_POST['synonym_scale'])) {
				$search->setSynonymScale($_POST['synonym_scale']);
			}
			if ($collapse !== '') {
				$search->setCollapse($collapse, $request->getPost('collapse_count', 1));
			}
			$search->setScwsMulti($scwsMulti);
			// query, range
			$search->setQuery($query);
			if ($range !== '' && strpos($range, '..') !== false) {
				$this->addSearchRange($search, $range);
			}
			$search->setLimit($limit, $offset);
			// sort
			if (isset($sort) && $sort !== '') {
				$sorts = array();
				foreach (explode(',', $sort) as $value) {
					$sorts[ltrim($value, '+')] = $value[0] === '+';
				}
				$search->setSort($sorts);
			}
			// highlight
			if ($highlight !== '') {
				$highlight = explode(',', $highlight);
			}
			// do search
			$docs = $search->search();
			$result['total'] = $search->getDbTotal();
			$result['count'] = $search->getLastCount();
			$result['docs'] = array();
			foreach ($docs as $doc) {
				$item = $doc->getFields();
				if (is_array($highlight)) {
					foreach ($highlight as $hk) {
						$item[$hk] = $search->highlight($item[$hk], true);
					}
				}
				$item['_rank'] = $doc->rank();
				$item['_percent'] = $doc->percent();
				$item['_weight'] = $doc->weight();
				$item['_ccount'] = $doc->ccount();
				$result['docs'][] = $item;
			}
			// add corrected
			if ($corrected === 'yes') {
				$result['corrected'] = $search->getCorrectedQuery();
			}
			// add related
			if ($related === 'yes') {
				$result['related'] = $search->getRelatedQuery();
			}
		} catch (XSException $e) {
			$result['result'] = false;
			if ($e->getCode() === CMD_ERR_NODB) {
				$result['total'] = 0;
				$result['count'] = 0;
				$result['docs'] = array();
			} else {
				throw new CHttpException(500, $e->getMessage());
			}
		}
		echo CJSON::encode($result);
	}

	/**
	 * 计数搜索
	 */
	public function actionCount()
	{
		try {
			// parameters
			$request = Yii::app()->request; /* @var $request CHttpRequest */
			$fuzzy = $request->getPost('fuzzy', 'no');
			$cutPercent = $request->getPost('cut_percent', 0);
			$cutWeight = $request->getPost('cut_weight', 0);
			$synonym = $request->getPost('synonym', 'no');
			$scwsMulti = $request->getPost('scws_multi', 3);
			$range = $request->getPost('range', '');
			$query = $request->getPost('query', '');
			// search
			$search = Yii::app()->pf->get($this->projectName)->search; /* @var $search XSSearch */
			$search->setFuzzy($fuzzy === 'yes');
			$search->setAutoSynonyms($synonym === 'yes');
			$search->setScwsMulti($scwsMulti);
			$search->setCutOff($cutPercent, $cutWeight);
			// query
			if (!is_array($query)) {
				$search->setQuery($query);
				if ($range !== '' && strpos($range, '..') !== false) {
					$this->addSearchRange($search, $range);
				}
				$count = $search->count();
			} else {
				$count = array();
				foreach ($query as $key => $value) {
					$count[$key] = $search->count($value);
				}
			}
		} catch (XSException $e) {
			if ($e->getCode() === CMD_ERR_NODB) {
				if (!is_array($query)) {
					$count = 0;
				} else {
					$count = array();
					foreach ($query as $key => $value) {
						$count[$key] = 0;
					}
				}
			} else {
				throw new CHttpException(500, $e->getMessage());
			}
		}
		echo CJSON::encode(array('result' => true, 'count' => $count));
	}

	/**
	 * 热门搜索
	 */
	public function actionHot()
	{
		try {
			$request = Yii::app()->request; /* @var $request CHttpRequest */
			$type = $request->getPost('type', 'total');
			if ($type !== 'currnum' && $type !== 'lastnum' && $type !== 'total') {
				throw new CHttpException(406, 'Incorrect value of type');
			}
			$limit = $request->getPost('limit', 6);
			$search = Yii::app()->pf->get($this->projectName)->search; /* @var $search XSSearch */
			$words = $search->getHotQuery($limit, $type);
			echo CJSON::encode(array('result' => true, 'words' => $words));
		} catch (XSException $e) {
			throw new CHttpException(500, $e->getMessage());
		}
	}

	/**
	 * 搜索建议
	 */
	public function actionSuggest()
	{
		try {
			$request = Yii::app()->request; /* @var $request CHttpRequest */
			$query = trim($request->getPost('query', ''));
			if ($query === '') {
				throw new CHttpException(406, 'Wrong parameter');
			}
			$limit = $request->getPost('limit', 10);
			$search = Yii::app()->pf->get($this->projectName)->search; /* @var $search XSSearch */
			$words = $search->getExpandedQuery($query, $limit);
			echo CJSON::encode(array('result' => true, 'words' => $words));
		} catch (XSException $e) {
			throw new CHttpException(500, $e->getMessage());
		}
	}

	/**
	 * This is the action to handle external exceptions.
	 */
	public function actionError()
	{
		$error = Yii::app()->errorHandler->error;
		echo CJSON::encode(array('result' => false, 'code' => $error['code'], 'message' => $error['message']));
	}

	/**
	 * 创建 API 调用 URL，并去除 projectName 参数
	 */
	public function createApiUrl($route = null, $schema = '')
	{
		$saveProject = $this->projectName;
		$this->projectName = null;
		$url = $this->createAbsoluteUrl($route === null ? $this->getRoute() : $route, array(), $schema);
		$this->projectName = $saveProject;
		return $url;
	}

	/**
	 * @param $search XSSearch
	 * @param $range value
	 * @return XSSearch
	 */
	private function addSearchRange($search, $value)
	{
		foreach (explode(',', $value) as $range) {
			if (($pos2 = strpos($range, '..')) === false) {
				continue;
			}
			$pos1 = strpos($range, ':');
			$field = substr($range, 0, $pos1);
			$from = substr($range, $pos1 + 1, $pos2 - $pos1 - 1);
			$to = substr($range, $pos2 + 2);
			if ($from === '' && $to === '') {
				continue;
			}
			$search->addRange($field, $from === '' ? null : $from, $to === '' ? null : $to);
		}
		return $search;
	}
}
